package com.cea.papyrus.profile.definition;

import java.util.ArrayList;
import java.util.Iterator;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EAnnotation;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EDataType;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EcoreFactory;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.dialogs.ElementTreeSelectionDialog;
import org.eclipse.uml2.uml.Class;
import org.eclipse.uml2.uml.Classifier;
import org.eclipse.uml2.uml.DataType;
import org.eclipse.uml2.uml.Element;
import org.eclipse.uml2.uml.Enumeration;
import org.eclipse.uml2.uml.Package;
import org.eclipse.uml2.uml.Profile;
import org.eclipse.uml2.uml.Property;
import org.eclipse.uml2.uml.Type;

/**
 * <p>this class is used to manage the redefinition of profiles:</p>
 * The normal definition in UML is like this:
 *	<ul>
 *  <li>Stereotype-->Eclass</li>
 *	<li>Enumeration-->EEnumeration :local copy
 *	<li>Datatype-->EClass</li>
 *	<li>Property--> EReference or EAttribute with ref on local copy</li>
 *  <li>PrimitiveType-> Edatatype : local copy</li> 
 *	</ul>
 * In papyrus:
 * <ul>
 * 	<li>Stereotype-->Eclass
 *	<li>Enumeration-->EEnumeration:local copy
 *	<li>Datatype-->EDatatype: local copy
 *	<li>Property--> EReference or EAttribute with ref on direct definition
 *	<li>PrimitiveType-> Edatatype : local copy 
 * </ul>
 */
public class ProfileRedefinition {
	
	/**
	 * this method is used to verify if does not exist doublon in properties of class
	 * It may come from extension addings.
	 * @param themodel the we want to verify
	 */
	public static void verifyBaseProperty(Package themodel){
		// the list of properties that have a problem
		ArrayList<Property>errorList= new ArrayList<Property>();

		//for each class
		Iterator<Element> it= themodel.allOwnedElements().iterator();
		while (it.hasNext()){
			Element eObject=it.next();
			if (eObject instanceof org.eclipse.uml2.uml.Class){
				verifyClassAtrribute((Class)eObject,errorList);
			}
		}
		// if there is a doublon display a popup
		if (errorList.size()>0){
			//result thanks to dialog
			ElementTreeSelectionDialog dialog = new ElementTreeSelectionDialog( new Shell(),
					new ErrorLabelProvider(), 
					new ErrorContentProvider());
			dialog.setInput(errorList);
			dialog.setTitle("Properties problem");

			dialog.setBlockOnOpen(true);
			dialog.setMessage("it exists "+errorList.size()+" errors.");
			dialog.open();
		}
	}

	/**
	 * this method is used to collect all properties doublon
	 * @param umlClass a given class where attributes are checked
	 * @param errorList where doublon properties are stored
	 */
	public static void verifyClassAtrribute(Class umlClass,ArrayList<Property> errorList) {
		//copy of attribute
		ArrayList<Property> propertyList=new ArrayList<Property>();
		Iterator<Property> propertyIter=umlClass.getAllAttributes().iterator();
		while(propertyIter.hasNext()){
			propertyList.add(propertyIter.next());
		}

		// find doublon!
		int index=0;
		while(index<propertyList.size()){
			Property currentProperty= propertyList.remove(index);
			//look for a property that is the same that currentproperty
			Iterator<Property> iterprop= propertyList.iterator();
			while(iterprop.hasNext()){
				Property pp= iterprop.next();
				if( pp.getName().equals(currentProperty.getName())){
					errorList.add(currentProperty);
				}
			}

		}

	}

	/**
	 * this method is used to redefine profile even if it contains subpackages or subprofiles
	 * @param thepackage the given profile that we want to define
	 * @param definitionAnnotation the definition annotation that is used to create the version annotation
	 */
	public static void redefineProfile(Package thepackage, PapyrusDefinitionAnnotation definitionAnnotation){
		//he wants to define
		if(thepackage instanceof Profile){
			Profile profile=(Profile)thepackage;
			// get profile definition
			EPackage profileDefinition = profile.getDefinition();
			//collect all EClassifier of the definition
			ArrayList<EClassifier> tempList = new ArrayList<EClassifier>();
			for(int i=0; i<profileDefinition.getEClassifiers().size(); i++){
				tempList.add((EClassifier)profileDefinition.getEClassifiers().get(i));
			}

			// for all EClass
			Iterator<EClassifier> eClassIter = tempList.iterator();
			while(eClassIter.hasNext()){
				EClassifier eclassifier=eClassIter.next();
				if(eclassifier instanceof EClass && !isDataTypeDefinition(eclassifier)){
					//redefine Eclass
					redefineEclass((EClass)eclassifier);
				}
			}
			
			// add profile definition annotation
			if(definitionAnnotation!=null) {
				profile.getDefinition().getEAnnotations().add(definitionAnnotation.convertToEAnnotation());
			}
		}
		Iterator<Package> it= thepackage.getNestedPackages().iterator();
		while(it.hasNext()){
			Package p=it.next();		
			ProfileRedefinition.redefineProfile(p, definitionAnnotation); 
		}
	}

	/**
	 * redefine only real definition or do nothing
	 * @param eclass the given eclass that we want to redefine
	 */
	public static void redefineEclass(EClass eclass){
		if(isADirectDefinition(eclass)){
			//1. redefine inheritances
			EList<EClass> eSuperTypes = eclass.getESuperTypes();

			/* copy in order to avoid concurrent access */
			ArrayList<EClass> superTypesList = new ArrayList<EClass>();
			for(int j=0; j<eSuperTypes.size(); j++){
				superTypesList.add((EClass)eSuperTypes.get(j));
			}
			//for each super types :we test if this is a direct definition
			//if not we remove the local copy and replace by direct definition
			Iterator<EClass> superIter = superTypesList.iterator();
			while(superIter.hasNext()){
				EClass currentSuperClass = superIter.next();
				if( !isADirectDefinition(currentSuperClass)){
					EClass directSuperClass= (EClass)lookForDirectDefinitionFrom(currentSuperClass);
					eclass.getESuperTypes().remove(currentSuperClass);
					eclass.getESuperTypes().add(directSuperClass);
				}    
			}
			//2.redefine eReferences
			//temp copy of the list
			Iterator<EReference> iterReference= eclass.getEReferences().iterator();
			ArrayList<EReference> referenceList = new ArrayList<EReference>();
			while(iterReference.hasNext()){
				referenceList.add(iterReference.next());
			}
			//for each reference of the EClass
			Iterator<EReference> refIterator=referenceList.iterator();
			while(refIterator.hasNext()){
				redefineEReference(refIterator.next(),eclass.getEPackage());
			}
			//3. redefine EAttributes
			Iterator<EAttribute> eattIterator= eclass.getEAllAttributes().iterator();
			//for each reference of the EClass
			while(eattIterator.hasNext()){
				redefineEAttribute(eattIterator.next(),eclass.getEPackage());
			}
		}
	}
	/**
	 * this class is used to redefine EReference with direct definition
	 * @param eReference the given EReference that we want to redefine
	 */
	public static void redefineEReference(EReference eReference,EPackage profileDefinition){
		EReference oldEOpposite=eReference.getEOpposite();
		EClassifier oldType=eReference.getEType();

		//2.1 the type is an EClass
		if(oldType instanceof EClass){
			//redefine type
			eReference.setContainment(false);
			if(!isDataTypeDefinition(oldType)){
				eReference.setEType(lookForDirectDefinitionFrom((EClass)oldType));
				//redefine the Opposite
				if(oldEOpposite!=null)
					eReference.setEOpposite(lookForEquivalentEreference((EClass)eReference.getEType(), oldEOpposite));
			}
		}
		if (isDataTypeDefinition(oldType)){
			EAttribute eatt=createEAttribute(eReference.getEContainingClass(), eReference);
			eatt.setEType(createDataType(profileDefinition, getUMLClassifierFromDefinition(oldType)));
			eatt.setDefaultValue(eReference.getDefaultValue());
			eatt.setDefaultValueLiteral(eReference.getDefaultValueLiteral());
			eReference.getEContainingClass().getEStructuralFeatures().remove(eReference);
		}
	}

	/**
	 * this class is used to redefine EAttribute with direct definition
	 * @param eAttribute the given EAttribute that we want to redefine
	 * @param profileDefinition the epackage that contains the Eclass , container of this Eattribute
	 */
	public static void redefineEAttribute(EAttribute eAttribute,EPackage profileDefinition){
		EClassifier oldEType=eAttribute.getEType();
		if (isDataTypeDefinition(oldEType)){
			eAttribute.setEType(createDataType(profileDefinition, getUMLClassifierFromDefinition(oldEType)));
		}
	}

	/**
	 * return id this Eclass is the real Definition
	 * @param eclass the eclass that we want to test
	 * @return true if this is the real definition or not is this is a local copy
	 */
	public static boolean isADirectDefinition(EClass eclass){
		if(eclass.getEAnnotations().size()>0){
			EAnnotation eAnnotation= eclass.getEAnnotations().get(0);
			if (eAnnotation.getReferences().size()>0){
				if (!(eAnnotation.getReferences().get(0) instanceof org.eclipse.uml2.uml.Stereotype )){
					ErrorDialog.openError(new Shell(),
							"Definition Error Message",
							"Problem because of the definition of "+ eclass.getName()+" in "+eclass.getEPackage().getNsURI(),
							new Status(IStatus.ERROR,
									com.cea.papyrus.profile.Activator.PLUGIN_ID,
									"Internal Error"));
				}
				org.eclipse.uml2.uml.Stereotype theStereotype=(org.eclipse.uml2.uml.Stereotype)eAnnotation.getReferences().get(0);
				if (theStereotype.getDefinition().equals(eclass)){
					return true;
				}
			}
		}
		return false;	
	}

	/**
	 * look for the real definition of the stereotype where the EClass may be the definition
	 * @param eclass that maybe the real definition (maybe a local copy of definition) 
	 * @return the real definition or the itself
	 */
	public static EClassifier lookForDirectDefinitionFrom(EClassifier eClassifier){
		if(eClassifier.getEAnnotations().size()>0){
			EAnnotation eAnnotation= eClassifier.getEAnnotations().get(0);
			if (eAnnotation.getReferences().size()>0){
				org.eclipse.uml2.uml.Stereotype theStereotype=(org.eclipse.uml2.uml.Stereotype)eAnnotation.getReferences().get(0);
				return theStereotype.getDefinition();
			}
		}
		return eClassifier;	
	}
	/**
	 * this method is used to look for an EReference equivalent to a given EReference 
	 * same name and type
	 * @param eclass the given EClass where we look for the equivalent EReference
	 * @param eReference the given EReference
	 * @return the return EReference or null
	 */
	private static EReference lookForEquivalentEreference(EClass eclass, EReference eReference){
		Iterator<EReference> refIterator= eclass.getEReferences().iterator();
		while (refIterator.hasNext()){
			EReference currentEReference=refIterator.next();
			if(currentEReference.getName().equals(eReference.getName())){
				if(currentEReference.getEType().getName().endsWith(eReference.getEType().getName())){
					return currentEReference;}
			}
		}
		return null;
	}
	/**
	 * this method is used to suppress all local copy of EClass in each Profile.
	 * @param thePackage that we want to clean
	 */
	public static void cleanProfile(Package thePackage){
		if(thePackage instanceof Profile){
			Profile profile=(Profile)thePackage;
			// get profile definition
			EPackage profileDefinition = profile.getDefinition();

			//collect all EClassifier of the definition
			ArrayList<EClassifier> tempList = new ArrayList<EClassifier>();
			for(int i=0; i<profileDefinition.getEClassifiers().size(); i++){
				tempList.add((EClassifier)profileDefinition.getEClassifiers().get(i));
			}

			// for all EClass
			Iterator<EClassifier> eClassIter = tempList.iterator();
			while(eClassIter.hasNext()){
				EClassifier eclassifier=eClassIter.next();

				if(eclassifier instanceof EClass){
					if (isDataTypeDefinition(eclassifier)){
						profileDefinition.getEClassifiers().remove(eclassifier);
					}

					//this is a direct Definition?
					else if (!isADirectDefinition((EClass)eclassifier)){
						//no, so it is removed
						profileDefinition.getEClassifiers().remove(eclassifier);
					}

				}
			}
		}
		Iterator<Package> it= thePackage.getNestedPackages().iterator();
		while(it.hasNext()){
			Package p=it.next();		
			ProfileRedefinition.cleanProfile(p); 
		}
	}

	/**
	 * Create a Edataptype from a datatype if it does not exist
	 * @param profileDefinition the Epackage where we want to create the Edatatype
	 * @param currentUmlType from this, the Edatatype will be created
	 * @return the created datatype 
	 */
	public static EDataType createDataType(EPackage profileDefinition, Type currentUmlType) {
		/* if eDataType already exists */
		Iterator<EClassifier> iter = profileDefinition.getEClassifiers().iterator();
		while(iter.hasNext()){
			EClassifier current = iter.next();
			if(current instanceof EDataType){
				if(current.getName().equals(currentUmlType.getName()))
					return (EDataType)current;
			}
		}
		//no, it have to be created
		EDataType dataTypeDef = EcoreFactory.eINSTANCE.createEDataType();
		dataTypeDef.setName(currentUmlType.getName());
		dataTypeDef.setInstanceClassName("java.lang.String");
		dataTypeDef.setSerializable(true);

		EAnnotation umlAnnotation = EcoreFactory.eINSTANCE.createEAnnotation();
		umlAnnotation.setSource("http://www.eclipse.org/uml2/2.0.0/UML");
		umlAnnotation.getReferences().add(currentUmlType);

		dataTypeDef.getEAnnotations().add(umlAnnotation);

		profileDefinition.getEClassifiers().add(dataTypeDef);
		return dataTypeDef;
	}


	/**
	 * this method is used to created an EAttribute from an Ereference
	 * @param container the Eclass that will contain the eattribute
	 * @param eReference from this, the eattribute will be created
	 * @return the created Eattribute
	 */
	public static EAttribute createEAttribute(EClass container, EReference eReference) {

		EAttribute eAttribute = EcoreFactory.eINSTANCE.createEAttribute();
		eAttribute.setEType(eReference.getEType());
		eAttribute.setName(eReference.getName());
		eAttribute.setChangeable(eReference.isChangeable());
		eAttribute.setLowerBound(eReference.getLowerBound());
		eAttribute.setUpperBound(eReference.getUpperBound());
		eAttribute.setOrdered(eReference.isOrdered());
		eAttribute.setDerived(eReference.isDerived());
		eAttribute.setTransient(eReference.isTransient());
		eAttribute.setUnique(eReference.isUnique());
		eAttribute.setUnsettable(eReference.isUnsettable());
		eAttribute.setVolatile(eReference.isVolatile());
		container.getEStructuralFeatures().add(eAttribute);
		return eAttribute;
	}

	/**
	 * test if the Eclass is a definition of a Datatype
	 * @param eclass that we want to test
	 * @return true if this is the defintion of a Datatype, false other
	 */
	public static boolean isDataTypeDefinition(EClassifier eclass){
		if(eclass.getEAnnotations().size()>0){
			EAnnotation eAnnotation= eclass.getEAnnotations().get(0);
			if (eAnnotation.getReferences().size()>0){
				org.eclipse.uml2.uml.Classifier theClassifier=(org.eclipse.uml2.uml.Classifier)eAnnotation.getReferences().get(0);
				if(theClassifier instanceof DataType &&!(theClassifier instanceof Enumeration)){
					return true;}
			}
		}
		return false;
	}

	/**
	 * this method is used to obtain the classifier from its definition
	 * @param eclass that is a definition
	 * @return the classifier that produce this definition
	 */
	public static Classifier getUMLClassifierFromDefinition(EClassifier eclass){
		if(eclass.getEAnnotations().size()>0){
			EAnnotation eAnnotation= eclass.getEAnnotations().get(0);
			if (eAnnotation.getReferences().size()>0){
				org.eclipse.uml2.uml.Classifier theClassifier=(org.eclipse.uml2.uml.Classifier)eAnnotation.getReferences().get(0);
				return theClassifier;}
		}
		return null;
	}
}